
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.SystemColor;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.HashMap;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextArea;
import javax.swing.ScrollPaneConstants;
import javax.swing.UIManager;
import javax.swing.border.LineBorder;
import javax.swing.border.TitledBorder;
import java.awt.Insets;

/**
 * Create entire graphical interface, including all interactive functionality
 */
public class GUI implements Observer {
	// Main interface frame. Resizeable, thanks to Swing's awesome layout options
	JFrame depotFrame;
	// All text area boxes, so they can be set from the update method
	JTextArea currentCustomerBox;
	JTextArea nextCustomerBox;
	JTextArea parcelsBox;
	JTextArea eventsBox;
	// Workers Panel, central, contains info for all workers
	JPanel workersPanel;
	// Buttons! 
	JButton addWorkerButton;
	JButton beginWorkButton;
	JButton increaseSpeedButton;
	JButton decreaseSpeedButton;
	JSplitPane splitPane;

	// Other bits of text which might need updating
	JLabel sleepLabel;
	JLabel parcelsCount;
	// Main objects containing queue of customers and list of parcels in depot
	CustomerQueue queue;
	ParcelList parcels;
	// Sleep time for all workers. 4 seconds by default.
	int sleepTime = 1;
	// Keep track of number of workers
	int workerCount = 0;

	// Create a HashMap to store Worker references
	Map<String, Worker> workers = new HashMap<String, Worker>();
	// Another HashMap to store Thread references
	Map<String, Thread> workerThreads = new HashMap<String, Thread>();
	// Cool progress bars which move backwards and forwards indeterminately!
	Map<String, JProgressBar> workerProgressBars = new HashMap<String, JProgressBar>();
	// Text boxes to contain the worker status
	Map<String, JTextArea> workerTextBoxes = new HashMap<String, JTextArea>();

	/** Constructor, takes in a Runnable CustomerQueue and thread, the worker and thread, and the list of parcels
	 */
	public GUI(CustomerQueue queue, ParcelList parcels) {
		this.queue=queue;
		this.parcels=parcels;
		// Create the GUI. Basically does everything.
		this.initialize();
		// Update non-observable-specific display elements
		this.updateDisplay();
		// Make the window visible
		this.depotFrame.setVisible(true);
	}

	private void addWorker() {
		try {
			// Give the Worker object a unique sequential identifier for HashMaps
			final String workerIdentifier = "worker"+ ++workerCount;
			// Create a new worker object taking in the list of customers & list of parcels
			// Also pass it the unique identifier for easier logging
			Worker newWorker = new Worker(queue,parcels,workerIdentifier);
			// Add new Worker instance to the workers HashMap using the unique identifier
			workers.put(workerIdentifier, newWorker);		
			// Create runnable thread for the worker object, allowing simple execution control
			workerThreads.put(workerIdentifier, new Thread(newWorker) );
			// Make this GUI observe the newly create Worker for changes
			newWorker.addObserver(this);
			// Push sleepTime to worker
			newWorker.setSleep(sleepTime);

			JTextArea newWorkerTextBox;
			JProgressBar newWorkerBar;

			final JPanel newWorkerPanel = new JPanel();
			newWorkerPanel.setForeground(Color.WHITE);
			newWorkerPanel.setBackground(Color.BLACK);
			newWorkerPanel.setLayout(new BorderLayout(0, 0));

			newWorkerTextBox = new JTextArea();
			newWorkerTextBox.setMargin(new Insets(10, 10, 10, 10));
			newWorkerTextBox.setEditable(false);
			newWorkerTextBox.setBackground(Color.WHITE);
			newWorkerTextBox.setForeground(Color.BLACK);
			newWorkerPanel.add(newWorkerTextBox, BorderLayout.CENTER);

			newWorkerBar = new JProgressBar();
			newWorkerBar.setIndeterminate(false);
			newWorkerBar.setBackground(SystemColor.textInactiveText);
			newWorkerBar.setForeground(SystemColor.textHighlight);
			newWorkerPanel.add(newWorkerBar, BorderLayout.NORTH);

			// Add controls to bottom of worker panel
			JPanel newWorkerControls = new JPanel();
			newWorkerPanel.setForeground(SystemColor.control);
			newWorkerPanel.setBackground(SystemColor.controlShadow);
			newWorkerPanel.add(newWorkerControls,BorderLayout.SOUTH);
			// Give controls panel a FlowLayout to align control buttons
			FlowLayout fl_newWorkerControls = new FlowLayout(FlowLayout.CENTER, 5, 5);
			fl_newWorkerControls.setAlignOnBaseline(true);
			newWorkerControls.setLayout(fl_newWorkerControls);

			final JButton breakButton = new JButton("Start Break");
			breakButton.setEnabled(true);
			breakButton.addActionListener(new ActionListener() {
				public void actionPerformed(ActionEvent arg0) {
					if (breakButton.getText()=="Start Break"){
						breakButton.setText("Finish Break");
						suspendWorker(workerIdentifier);
					}else if (breakButton.getText()=="Finish Break"){
						breakButton.setText("Start Break");
						resumeWorker(workerIdentifier);
					}
				}
			});
			newWorkerControls.add(breakButton);

			JButton finishWorkButton = new JButton("Finish Work");
			finishWorkButton.setEnabled(true);
			finishWorkButton.addActionListener(new ActionListener() {
				public void actionPerformed(ActionEvent arg0) {
					// Stop threads before closing
					stopWorker(workerIdentifier);
				}
			});
			newWorkerControls.add(finishWorkButton);

			JButton removeWorkerButton = new JButton("Remove Worker");
			removeWorkerButton.setEnabled(true);
			removeWorkerButton.addActionListener(new ActionListener() {
				public void actionPerformed(ActionEvent arg0) {
					// Ensure worker is stopped before removing GUI panel
					stopWorker(workerIdentifier);
					workersPanel.remove(newWorkerPanel);
				}
			});
			newWorkerControls.add(removeWorkerButton);

			// Add worker panel to workers panel in frame
			workersPanel.add(newWorkerPanel);

			// Add dynamic GUI elements to HashMaps so we can update them
			workerTextBoxes.put(workerIdentifier, newWorkerTextBox);
			workerProgressBars.put(workerIdentifier, newWorkerBar);

			// Update non-observable-specific display elements
			updateDisplay();

			// Run worker thread
			newWorkerBar.setIndeterminate(true);
			workerThreads.get(workerIdentifier).start();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private void shutdown() {
		// Clean up threads and free memory
		shutdownWorkers();
		// Write main log file
		Logger.INSTANCE.appendLog("main", "\n-> Shutdown requested, final statistics below:\n" );
		Logger.INSTANCE.appendLog("main", "\n--> Remaining parcels in depot:\n"+parcels.outputParcels() );
		Logger.INSTANCE.appendLog("main", "\n--> Remaining customers in queue: "+queue.getSize() );
		Logger.INSTANCE.writeLog("main", "mainLog.txt");
		// Write thread log file
		Logger.INSTANCE.writeLog("workerEvents", "workerEvents.txt");
		// Close window
		depotFrame.dispose();
	}
	
	private void shutdownWorkers() {
		for (String currentWorkerIdentifier : workers.keySet()) {
			stopWorker(currentWorkerIdentifier);
		}
	}

	private void stopWorker(String workerIdentifier) {
		workerProgressBars.get(workerIdentifier).setIndeterminate(false);
		workerThreads.get(workerIdentifier).interrupt();
	}

	@SuppressWarnings("deprecation")
	private void suspendWorker(String workerIdentifier) {
		workerProgressBars.get(workerIdentifier).setIndeterminate(false);
		workerThreads.get(workerIdentifier).suspend();
	}

	@SuppressWarnings("deprecation")
	private void resumeWorker(String workerIdentifier) {
		workerProgressBars.get(workerIdentifier).setIndeterminate(true);
		workerThreads.get(workerIdentifier).resume();
	}

	private void updateWorkersSleep() {
		for (String currentWorkerIdentifier : workers.keySet()) {
			workers.get(currentWorkerIdentifier).setSleep(sleepTime);
		}
	}

	/**
	 * Create the frame and everything in it. Colours are all hard-coded, sadly
	 */
	private void initialize() {
		// Most of this GUI was made with the assistance of Eclipse WindowBuilder. I suggest using it, for everything.
		depotFrame = new JFrame();
		depotFrame.getContentPane().setBackground(SystemColor.controlShadow);
		depotFrame.setTitle("Depot Simulator");
		depotFrame.setBackground(SystemColor.window);
		depotFrame.setBounds(100, 100, 1000, 800);
		depotFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
		// Make sure we can actually close using the window close button
		depotFrame.addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent arg0) {
				try{
					shutdown();
				} catch(NullPointerException npe){
					System.exit(0);
				}
			}
		});
		depotFrame.getContentPane().setLayout(new BorderLayout(10, 10));

		// So, we have a frame. Let's start making panels and text areas
		JPanel controlsBox = new JPanel();
		controlsBox.setForeground(SystemColor.control);
		controlsBox.setBackground(SystemColor.controlShadow);
		controlsBox.setBorder(new TitledBorder(new LineBorder(new Color(100, 100, 100), 1, true), "Controls", TitledBorder.LEADING, TitledBorder.TOP, null, new Color(255, 255, 255)));
		// Controls box layout
		FlowLayout fl_controlsBox = new FlowLayout(FlowLayout.CENTER, 5, 5);
		fl_controlsBox.setAlignOnBaseline(true);
		controlsBox.setLayout(fl_controlsBox);
		// Add controls box to top of frame
		depotFrame.getContentPane().add(controlsBox, BorderLayout.NORTH);

		// Add control buttons
		addWorkerButton = new JButton("Add Worker");
		addWorkerButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent arg0) {
				addWorker();
			}
		});
		controlsBox.add(addWorkerButton);

		JButton finishWorkButton = new JButton("Finish Work");
		finishWorkButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent arg0) {
				shutdown();
			}
		});
		controlsBox.add(finishWorkButton);

		// Increase Speed: decrease delay by 1 second
		increaseSpeedButton = new JButton("Increase Speed");
		increaseSpeedButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent arg0) {
				// Increase speed: decrement sleepTime by 1 second as long as above 0
				if ( (sleepTime-1) > 0) {
					sleepTime -= 1;
				}
				// Push new sleepTime to all workers
				updateWorkersSleep();
				// Update non-observable-specific display elements
				updateDisplay();
			}
		});
		increaseSpeedButton.setEnabled(true);
		controlsBox.add(increaseSpeedButton);
		decreaseSpeedButton = new JButton("Decrease Speed");
		decreaseSpeedButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				// Increase sleepTime by 1 second
				sleepTime++;
				// Push new sleepTime to all workers
				updateWorkersSleep();
				// Update non-observable-specific display elements
				updateDisplay();
			}
		});
		decreaseSpeedButton.setEnabled(true);
		controlsBox.add(decreaseSpeedButton);

		sleepLabel = new JLabel("");
		sleepLabel.setForeground(Color.WHITE);
		controlsBox.add(sleepLabel);

		workersPanel = new JPanel();
		workersPanel.setForeground(SystemColor.control);
		workersPanel.setBackground(SystemColor.controlShadow);
		workersPanel.setBorder(new TitledBorder(new LineBorder(new Color(100, 100, 100), 1, true), "Workers", TitledBorder.LEFT, TitledBorder.TOP, null, new Color(255, 255, 255)));
		// Give the workers panel a FlowLayout to allow multiple workers to fit in neatly
		FlowLayout fl_workersPanel = new FlowLayout(FlowLayout.CENTER, 5, 5);
		fl_workersPanel.setAlignOnBaseline(true);
		workersPanel.setLayout(fl_workersPanel);
		// Add workers panel to frame
		depotFrame.getContentPane().add(workersPanel, BorderLayout.CENTER);

		JPanel customerPanel = new JPanel();
		customerPanel.setForeground(SystemColor.control);
		customerPanel.setBackground(SystemColor.controlShadow);
		customerPanel.setBorder(new TitledBorder(new LineBorder(new Color(100, 100, 100), 1, true), "Customer Queue", TitledBorder.LEADING, TitledBorder.TOP, null, new Color(255, 255, 255)));
		depotFrame.getContentPane().add(customerPanel, BorderLayout.SOUTH);
		customerPanel.setLayout(new GridLayout(1, 2, 10, 0));

		currentCustomerBox = new JTextArea();
		currentCustomerBox.setMargin(new Insets(10, 10, 10, 10));
		currentCustomerBox.setRows(7);
		currentCustomerBox.setEditable(false);
		currentCustomerBox.setBackground(SystemColor.info);
		customerPanel.add(currentCustomerBox);

		nextCustomerBox = new JTextArea();
		nextCustomerBox.setMargin(new Insets(10, 10, 10, 10));
		nextCustomerBox.setRows(7);
		nextCustomerBox.setEditable(false);
		nextCustomerBox.setBackground(SystemColor.text);
		customerPanel.add(nextCustomerBox);

		JPanel eventsPanel = new JPanel();
		eventsPanel.setForeground(SystemColor.control);
		eventsPanel.setBackground(SystemColor.controlShadow);
		eventsPanel.setBorder(new TitledBorder(new LineBorder(new Color(100, 100, 100), 1, true), "Events", TitledBorder.LEADING, TitledBorder.TOP, null, new Color(255, 255, 255)));
		depotFrame.getContentPane().add(eventsPanel, BorderLayout.WEST);
		eventsPanel.setLayout(new BorderLayout(0, 0));

		JScrollPane scrollPane = new JScrollPane();
		scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
		eventsPanel.add(scrollPane);

		eventsBox = new JTextArea();
		eventsBox.setWrapStyleWord(true);
		eventsBox.setColumns(20);
		eventsBox.setMargin(new Insets(10, 10, 10, 10));
		eventsBox.setFont(UIManager.getFont("TextPane.font"));
		eventsBox.setLineWrap(true);
		eventsBox.setEditable(false);
		eventsBox.setBackground(SystemColor.controlShadow);
		eventsBox.setForeground(SystemColor.control);
		scrollPane.setViewportView(eventsBox);

		JPanel parcelsPanel = new JPanel();
		parcelsPanel.setBorder(new TitledBorder(new LineBorder(new Color(100, 100, 100), 1, true), "Parcels", TitledBorder.LEADING, TitledBorder.TOP, null, new Color(255, 255, 255)));
		parcelsPanel.setBackground(SystemColor.controlShadow);
		parcelsPanel.setForeground(SystemColor.control);
		depotFrame.getContentPane().add(parcelsPanel, BorderLayout.EAST);
		parcelsPanel.setLayout(new BorderLayout(0, 0));

		parcelsCount = new JLabel("Still in Depot:");
		parcelsPanel.add(parcelsCount, BorderLayout.NORTH);
		parcelsCount.setBackground(SystemColor.controlHighlight);
		parcelsCount.setForeground(SystemColor.textHighlightText);

		JScrollPane parcelsScrollPane = new JScrollPane();
		parcelsScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
		parcelsPanel.add(parcelsScrollPane, BorderLayout.CENTER);

		parcelsBox = new JTextArea();
		parcelsBox.setMargin(new Insets(10, 10, 10, 10));
		parcelsScrollPane.setViewportView(parcelsBox);
		parcelsBox.setBackground(SystemColor.controlShadow);
		parcelsBox.setForeground(SystemColor.control);
	}

	// As the GUI is an observer, any time another part of the program (read: worker, queue, parcels) changes, this will be called
	// This allows us to update the interface instantly whenever there is a change without wasted redrawing
	public void update(Observable observableObject, Object workerIdentifier) {
		// Update display of current and next customer in top boxes, directly from queue
		this.currentCustomerBox.setText( queue.getAtPosition(1) == null ? 	"No more customers" : 
			"Current Customer: \nName: "
			+ queue.getAtPosition(1).getName().getFullName()+
			"\nParcel: "+ queue.getAtPosition(1).getParcelId()+"\nStatus: Ready" );
		this.nextCustomerBox.setText( queue.getAtPosition(2) == null ? 	"No more customers" : 
			"Next Customer: \nName: "
			+ queue.getAtPosition(2).getName().getFullName()+
			"\nParcel: "+ queue.getAtPosition(2).getParcelId()+"\nStatus: Waiting" );

		// Update display of parcels still in the depot from parcel list
		parcelsBox.setText(parcels.outputParcels());
		parcelsCount.setText("Still in Depot: "+Integer.toString(parcels.getSize()));

		// NotifyObservers was given an argument, therefore it must have been called by a worker
		if(workerIdentifier != null) {
			// Update the worker status panel for this worker
			workerTextBoxes.get(workerIdentifier).setText(Logger.INSTANCE.getLog(workerIdentifier+"status"));

			// Check if worker is finished, remove progress bar
			if(Logger.INSTANCE.getLog(workerIdentifier+"status").equals("Worker Finished")) {
				workerProgressBars.get(workerIdentifier).setIndeterminate(false);			}
		}

		// Update non-observable-specific display elements
		updateDisplay();
	}

	private void updateDisplay() {
		// Update events box with any events provided by any workers
		eventsBox.setText(Logger.INSTANCE.getLog("workerEvents"));
		// Show current speed / sleep time in seconds
		sleepLabel.setText("Sleep Time: "+sleepTime+" secs");
		// Redraw the entire interface to update display
		depotFrame.repaint();
	}
}
